package parser;

import java.util.ArrayList;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

import storage.Storage;
import logic.*;
import global.*;

/**@author A0105661M*/

public class CommandParser {

    public static final int OPTION_MARKDONE = 1;
    public static final int OPTION_MARKUNDONE = 0;
    
	public static final boolean NORMAL_ADD = false;
	public static final boolean UPDATE_ADD = true;
	
	public static final int INVALID_INDEX = -1;	
	public static final int BOTH = 0;
	public static final int DONE_ONLY = 2;
	public static final int UNDONE_ONLY = 1;	
	
	public static final int SEARCHOPTION_IS_SEARCH = 0;
	public static final int SEARCHOPTION_IS_AFTER = 1;
	public static final int SEARCHOPTION_DATE_ADJUSTED = 2;
	public static final int SEARCHOPTION_IS_BEFORE = 3;
	
	public static final int SEARCH_TYPE = 0;
	public static final int SEARCH_IS_AFTER = 0;
	public static final int SEARCH_IS_BEFORE = 1;
	public static final int SEARCH_IS_ALL = 2;
	public static final int SEARCH_UNDONE = 0;	
	
	public static final int START_TIME = 0;
	public static final int END_TIME = 1;	
	
	public static final int NUM_OF_COMMAND_PARTS = 2;
	public static final int ONE = 1;
	
	private static String parserLogFileName = "parserLog.txt";
	private static Logger logger;
	private static FileHandler fh;
	
	public static Command analyzeCommand(String command) throws Exception {

		try{
			logger = Logger.getLogger(Storage.class.getName());
			fh = new FileHandler(parserLogFileName);
			logger.addHandler(fh);
			
			logger.log(Level.INFO,"Checking whether command is empty...");
			if (isEmptyInput(command)) {
				logger.log(Level.SEVERE,"Empty Command...");
				return createAddCommand(null, false, Constant.EMPTY_COMMAND);
			}

			logger.log(Level.INFO,"Deciding command type...");
			CommandType.Type commandType = determineCommandType(command);
			String commandContent = Methods.removeFirstWord(command);
			String[] commandConverted = Methods.stringToArray(Methods
					.removeFirstWord(command));

			logger.log(Level.INFO,"command type is known. Start analyzing command");
			switch (commandType) {
			case DISPLAY:
				return analyzeDisplayCommand(commandContent);
			case DELETE:
				return analyzeDeleteCommand(commandConverted);
			case MARKDONE:
				return analyzeMarkCommand(commandConverted,
						OPTION_MARKDONE);
			case MARKUNDONE:
				return analyzeMarkCommand(commandConverted,
						OPTION_MARKUNDONE);
			case UPDATE:
				return analyzeUpdateCommand(commandContent);
			case UNDO:
				return analyzeUndoCommand();
			case REDO:
				return analyzeRedoCommand();
			case OPEN:
				return analyzeOpenCommand();
			case IMPLICIT_ADD:
				return analyzeAddCommand(command, NORMAL_ADD);
			case CONNECT:
				return new ConnectCommand();
			case EXIT:
				exit();
			default:
				logger.log(Level.SEVERE,"unknown type");
				throw new Exception(Constant.UNRECOGNIZED_COMMAND_TYPE);
			}
		} catch (Exception e){
			logger.log(Level.INFO,"unknown error in parser...");
			Task task = null;
			boolean isExecutable = false;;
			String feedback = Constant.UNKNOWN_ERROR;
			return new AddCommand(task, isExecutable, feedback);
		}
	}

	private static void exit() {
		logger.log(Level.INFO,"User choose to exit the system...");
		if (!Storage.isRecordEmpty()) {
			Storage.storeRecord();
		}
		Storage.storeRecord();
		SystemFunction.NORMAL_EXIT();
	}

	private static Command analyzeMarkCommand(String[] commandContent, int type) {

		logger.log(Level.INFO,"parsing a markCommand...");
		boolean isMarkAll = NewComponentAnalyser.isAll(commandContent);
		boolean isExecutable = true;
		String feedback = Constant.EMPTY_STRING;

		ArrayList<Integer> indexArray = NewComponentAnalyser
				.analyzeIndex(commandContent);

		logger.log(Level.INFO,"invalid index for markCommand...");
		if (isMarkAll == false && indexArray.size() == 0) {
			isExecutable = false;
			feedback = Constant.INVALID_INPUT_MARK_COMMAND;
		}

		if (type == OPTION_MARKUNDONE
				|| type == OPTION_MARKDONE) {
			logger.log(Level.INFO,"valid markCommand...");
			return new MarkCommand(indexArray, isMarkAll, isExecutable,
					feedback, type);
		} else {
			logger.log(Level.INFO,"invlaid markCommand...");
			isExecutable = false;
			return new MarkCommand(indexArray, isMarkAll, isExecutable,
					feedback, type);
		}
	}

	private static Command analyzeOpenCommand() {
		logger.log(Level.INFO,"parsing OpenCommand...");
		boolean isExecutable = true;
		String feedback = Constant.EMPTY_STRING;
		return new OpenCalendarCommand(isExecutable, feedback);
	}

	private static Command analyzeRedoCommand() {
		logger.log(Level.INFO,"parsing RedoCommand...");
		boolean isExecutable = true;
		String feedback = Constant.EMPTY_STRING;
		return new RedoCommand(isExecutable, feedback);
	}

	private static Command analyzeUndoCommand() {
		logger.log(Level.INFO,"parsing UndoCommand...");
		boolean isExecutable = true;
		String feedback = Constant.EMPTY_STRING;
		return new UndoCommand(isExecutable, feedback);
	}

	private static Command analyzeUpdateCommand(String commandContent)
			throws Exception {

		logger.log(Level.INFO,"parsing UpdateCommand...");
		Task newTask = null;
		boolean isExecutable = false;
		String feedback = Constant.EMPTY_STRING;

		int index = getIndex(commandContent);
		String commandLeft = Methods.removeFirstWord(commandContent);

		if (index != INVALID_INDEX) {
			AddCommand newAdd = (AddCommand) analyzeAddCommand(commandLeft,
					UPDATE_ADD);
			if (newAdd.isExecutable()) {
				newTask = newAdd.getTask();
				isExecutable = true;
			}
		} else {
			logger.log(Level.SEVERE,"invalid indexes for updating command...");
			feedback += Constant.INVALID_INPUT_UPDATE_COMMAND;
		}

		return new UpdateCommand(index, newTask, isExecutable, feedback);
	}

	private static Command analyzeDeleteCommand(String[] commandContent) {

		logger.log(Level.INFO,"parsing deleteCommand...");
		boolean isExecutable = true;
		String feedback = Constant.EMPTY_STRING;

		boolean isDeleteAll = NewComponentAnalyser.isAll(commandContent);
		ArrayList<Integer> indexArray = NewComponentAnalyser
				.analyzeIndex(commandContent);

		if (isDeleteAll == false && indexArray.size() == 0) {
			logger.log(Level.SEVERE,"invalid indexes for delete command...");
			isExecutable = false;
			feedback = Constant.INVALID_INPUT_DELETE_COMMAND;
		}

		return new DeleteCommand(indexArray, isDeleteAll, isExecutable,
				feedback);
	}

	private static Command analyzeDisplayCommand(String commandContent)
			throws Exception {

		logger.log(Level.INFO,"parsing displayCommand...");
		boolean isExecutable = true;
		String[] feedback = new String[1];
		feedback[0] = Constant.EMPTY_STRING;

		String[] result = analyzeKeyword(commandContent);
		assert(result != null);
		String keyword = result[0];
		commandContent = result[1];

		String[] commandLeft = Methods.stringToArray(commandContent);

		int[] keyOptionsInt = new int[1];
		String[] keyOptionsString = new String[1];
		boolean[] keyOptionsBoolean = new boolean[3];
		analyzeSearchKeyOptions(keyOptionsInt, keyOptionsString,
				keyOptionsBoolean, commandLeft);

		boolean[] searchOption = initializeSearchOption(true,
				keyOptionsBoolean[SEARCH_IS_AFTER], false,
				keyOptionsBoolean[SEARCH_IS_BEFORE]);

		Time[] finalTime = analyzeTimeForSearching(commandLeft, feedback,
				searchOption, keyword, keyOptionsString[SEARCH_TYPE],
				keyOptionsInt[SEARCH_UNDONE],
				keyOptionsBoolean[SEARCH_IS_ALL]);

		if (errorExist(feedback[0])) {
			logger.log(Level.SEVERE,"error occurs when parsing DisplayCommand...");
			isExecutable = false;
		}

		return new DisplayCommand(keyword, finalTime[START_TIME],
				finalTime[END_TIME],
				keyOptionsString[SEARCH_TYPE],
				keyOptionsInt[SEARCH_UNDONE], isExecutable,
				feedback[0]);
	}

	private static void analyzeSearchKeyOptions(int[] keyOptionsInt,
			String[] keyOptionsString, boolean[] keyOptionsBoolean,
			String[] commandLeft) {

		logger.log(Level.INFO,"analyzing Search Key Options...");
		initializeKeyOptions(keyOptionsInt, keyOptionsString, keyOptionsBoolean);

		assert(commandLeft != null);
		for (int i = 0; i < commandLeft.length; i++) {
			String curWord = commandLeft[i].toLowerCase();
			if (curWord.equals("done")) {
				keyOptionsInt[SEARCH_UNDONE] = DONE_ONLY;

			} else if (curWord.equals("undone")) {
				keyOptionsInt[SEARCH_UNDONE] = UNDONE_ONLY;

			} else if (curWord.equals("timed") || curWord.equals("scheduled")) {
				keyOptionsString[SEARCH_TYPE] = Constant.TIME_TASK_TYPE;

			} else if (curWord.equals("untimed") || curWord.equals("float")) {
				keyOptionsString[SEARCH_TYPE] = Constant.FLOAT_TASK_TYPE;

			} else if (curWord.equals("deadline")) {
				keyOptionsString[SEARCH_TYPE] = Constant.DEADLINE_TASK_TYPE;

			} else if (curWord.equals("from") || curWord.equals("after")
					|| curWord.equals("start") || curWord.equals("begin")) {
				keyOptionsBoolean[SEARCH_IS_AFTER] = true;

			} else if (curWord.equals("by") || curWord.equals("before")) {
				keyOptionsBoolean[SEARCH_IS_BEFORE] = true;

			} else if (curWord.equals("all")) {
				keyOptionsBoolean[SEARCH_IS_ALL] = true;
			}
		}

	}

	private static Command analyzeAddCommand(String commandContent,
			boolean isUpdate) throws Exception {

		logger.log(Level.INFO,"analyzing Add Command...");
		String[] command = new String[NUM_OF_COMMAND_PARTS];
		String[] feedback = new String[ONE];
		feedback[0] = Constant.EMPTY_STRING;
		int taskType = Constant.UNKNOWN_TASK_TYPE_INT;
		int[] timeInfo;
		int[] dateInfo;
		String taskInformation = Constant.EMPTY_STRING;
		String FinalFeedback = Constant.EMPTY_STRING;
		boolean isLooseMode = false;

		commandContent = commandContent.trim();
		int semicolonIndex = commandContent.indexOf(';');

		if (isInStrictMode(semicolonIndex)) {

			command = divideCommandInfo(commandContent, semicolonIndex);
			if (isEmptyAddCommand(command)) {
				return createAddCommand(null, false, Constant.EMPTY_COMMAND);
			}

			taskInformation = command[0];
			String[] timingInfo = Methods.stringToArray(command[1]);
			boolean[] searchOption = initializeSearchOptionForAdd();

			// to be implemented
			int[] recurrenceInfo = NewComponentAnalyser.analyzeRecurrence(
					timingInfo, feedback);
			timeInfo = NewComponentAnalyser.analyzeTime(timingInfo,
					recurrenceInfo[0], feedback, searchOption, null);
			dateInfo = NewComponentAnalyser.analyzeDate(timingInfo,
					recurrenceInfo[0], feedback, searchOption);
			taskType = NewComponentAnalyser.decideTaskType(timingInfo,
					recurrenceInfo[0], feedback, timeInfo, dateInfo);
			FinalFeedback = feedback[0];
		} else {
			isLooseMode = true;
			String[] taskAndTimingInfo = Methods.stringToArray(commandContent);

			boolean[] searchOption = initializeSearchOptionForAdd();

			// to be implemented
			int[] recurrenceInfo = NewComponentAnalyser.analyzeRecurrence(
					taskAndTimingInfo, feedback);
			timeInfo = NewComponentAnalyser.analyzeTime(taskAndTimingInfo,
					recurrenceInfo[0], feedback, searchOption, null);
			dateInfo = NewComponentAnalyser.analyzeDate(taskAndTimingInfo,
					recurrenceInfo[0], feedback, searchOption);
			taskInformation = NewComponentAnalyser.extractTaskInfo(
					taskAndTimingInfo, recurrenceInfo[0], feedback, timeInfo,
					dateInfo);
			taskType = NewComponentAnalyser.decideTaskType(taskAndTimingInfo,
					recurrenceInfo[0], feedback, timeInfo, dateInfo);
			FinalFeedback = feedback[0];
		}

		if (taskType > Constant.UNKNOWN_TASK_TYPE_INT) {
			Task task;

			Time startingDateAndTime = Time.convertInfoToTime(dateInfo,
					timeInfo, START_TIME + 1);
			Time endingDateAndTime = Time.convertInfoToTime(dateInfo, timeInfo,
					END_TIME + 1);

			switch (taskType) {
			case Constant.DEADLINE_TASK_TYPE_INT:
				task = new Task(Constant.DEADLINE_TASK_TYPE, taskInformation,
						Constant.EMPTY_STRING, endingDateAndTime);
				break;
			case Constant.TIME_TASK_TYPE_INT:
				task = new Task(Constant.TIME_TASK_TYPE, taskInformation,
						Constant.EMPTY_STRING, startingDateAndTime,
						endingDateAndTime);
				break;
			case Constant.FLOAT_TASK_TYPE_INT:
				if (taskInformation.equals(Constant.EMPTY_STRING)) {
					return createAddCommand(null, false,
							Constant.INVALID_INPUT_FOR_FLOAT_TASK);
				}
				task = new Task(Constant.FLOAT_TASK_TYPE, taskInformation);
				if (isLooseMode && isUpdate) {
					setTaskTypeAsNull(task);
				}
				break;
			default:
				task = null;
			}

			return createAddCommand(task, true, FinalFeedback);

		} else {
			return createAddCommand(null, false, FinalFeedback);
		}
	}

	private static boolean isInStrictMode(int semicolonIndex) {
		return semicolonIndex != INVALID_INDEX;
	}

	private static void initializeKeyOptions(int[] keyOptionsInt,
			String[] keyOptionsString, boolean[] keyOptionsBoolean) {
		
		assert(keyOptionsInt != null);
		assert(keyOptionsString != null);
		assert(keyOptionsBoolean != null);
		
		logger.log(Level.INFO,"initializing Key Options...");
		
		keyOptionsBoolean[SEARCH_IS_AFTER] = false;
		keyOptionsBoolean[SEARCH_IS_BEFORE] = false;
		keyOptionsBoolean[SEARCH_IS_ALL] = false;
		keyOptionsString[SEARCH_TYPE] = null;
		keyOptionsInt[SEARCH_UNDONE] = BOTH;
	}

	private static String[] analyzeKeyword(String commandContent) {

		logger.log(Level.INFO,"analyzing keyword...");
		assert(commandContent != null);
		String[] result = new String[2];
		String keyword = Constant.EMPTY_STRING;

		int quoteIndex1 = commandContent.indexOf('\"');
		int quoteIndex2 = commandContent.indexOf('\"', quoteIndex1 + 1);

		if (quoteIndex1 != INVALID_INDEX
				&& quoteIndex2 != INVALID_INDEX) {
			keyword = commandContent.substring(quoteIndex1, quoteIndex2 + 1)
					.trim().split("\"")[1];
			commandContent = commandContent.substring(0, quoteIndex1)
					+ commandContent.substring(quoteIndex2,
							commandContent.length());
		}

		result[0] = keyword;
		result[1] = commandContent;
		return result;
	}

	private static Time[] analyzeTimeForSearching(String[] commandLeft,
			String[] feedback, boolean[] searchOption, String keyword,
			String type, int undoneOption, boolean isAll) {

		logger.log(Level.INFO,"analyzing Time For Searching...");
		int[] dateInfo = NewComponentAnalyser.analyzeDate(commandLeft,
				commandLeft.length, feedback, searchOption);
		int[] timeInfo = NewComponentAnalyser.analyzeTime(commandLeft,
				commandLeft.length, feedback, searchOption, dateInfo);
		Time[] finalTime = NewComponentAnalyser.adjustDateAndTimeForSearch(
				dateInfo, timeInfo, feedback, keyword, type, undoneOption,
				isAll);

		return finalTime;
	}

	private static int getIndex(String commandContent) {
		assert(commandContent != null);
		String indexWord = Methods.getFirstWord(commandContent);
		int index = Methods.safeParseInt(indexWord);

		return index;
	}

	private static boolean[] initializeSearchOption(boolean isSearch,
			boolean isAfter, boolean isAdjusted, boolean isBefore) {

		logger.log(Level.INFO,"initializing Search Option...");
		boolean[] searchOption = new boolean[4];
		searchOption[SEARCHOPTION_IS_SEARCH] = isSearch;
		searchOption[SEARCHOPTION_IS_AFTER] = isAfter;
		searchOption[SEARCHOPTION_DATE_ADJUSTED] = isAdjusted;
		searchOption[SEARCHOPTION_IS_BEFORE] = isBefore;

		return searchOption;
	}

	private static boolean errorExist(String feedback) {
		return feedback.indexOf(Constant.ERROR_SIGNAL) != INVALID_INDEX;
	}

	private static boolean[] initializeSearchOptionForAdd() {
		
		logger.log(Level.INFO,"initializing Search Option for add...");
		boolean[] searchOption = new boolean[4];
		searchOption[SEARCHOPTION_IS_SEARCH] = false;
		searchOption[SEARCHOPTION_IS_AFTER] = false;
		searchOption[SEARCHOPTION_DATE_ADJUSTED] = false;
		searchOption[SEARCHOPTION_IS_BEFORE] = false;

		return searchOption;
	}

	private static boolean isEmptyAddCommand(String[] command) {
		assert(command != null);
		return command[0].equals(Constant.EMPTY_STRING)
				&& command[1].equals(Constant.EMPTY_STRING);
	}

	private static String[] divideCommandInfo(String commandContent,
			int semicolonIndex) {
		String[] command = new String[NUM_OF_COMMAND_PARTS];
		command[0] = commandContent.substring(0, semicolonIndex);
		command[1] = commandContent.substring(semicolonIndex + 1,
				commandContent.length());

		return command;
	}

	private static void setTaskTypeAsNull(Task task) {
		assert(task != null);
		task.setTaskTypeAsNull();
	}

	private static Command createAddCommand(Task task, boolean isExecutable,
			String finalFeedback) {
		return new AddCommand(task, isExecutable, finalFeedback);
	}

	private static CommandType.Type determineCommandType(String command) {

		assert(command != null);
		logger.log(Level.INFO,"determining CommandType...");
		command = command.trim();
		String commandTypeString = Methods.getFirstWord(command);

		if (command == null)
			throw new Error(Constant.NULL_COMMAND_TYPE);

		if (command.equalsIgnoreCase("undo")) {
			return CommandType.Type.UNDO;
		} else if (command.equalsIgnoreCase("redo")) {
			return CommandType.Type.REDO;
		} else if (command.equalsIgnoreCase("calendar")
				|| command.equalsIgnoreCase("opencal")) {
			return CommandType.Type.OPEN;
		} else if (command.equalsIgnoreCase("exit")) {
			return CommandType.Type.EXIT;
		} else if (command.equalsIgnoreCase("connect")) {
			return CommandType.Type.CONNECT;
		}

		if (commandTypeString.equalsIgnoreCase("display")
				|| commandTypeString.equalsIgnoreCase("search")) {
			return CommandType.Type.DISPLAY;
		} else if (commandTypeString.equalsIgnoreCase("delete")) {
			return CommandType.Type.DELETE;
		} else if (commandTypeString.equalsIgnoreCase("update")) {
			return CommandType.Type.UPDATE;
		} else if (commandTypeString.equalsIgnoreCase("done")
				|| commandTypeString.equalsIgnoreCase("markdone")) {
			return CommandType.Type.MARKDONE;
		} else if (commandTypeString.equalsIgnoreCase("undone")
				|| commandTypeString.equalsIgnoreCase("markundone")) {
			return CommandType.Type.MARKUNDONE;
		} else {
			return CommandType.Type.IMPLICIT_ADD;
		}
	}

	private static boolean isEmptyInput(String command) {
		return command.trim().equals(Constant.EMPTY_STRING);
	}
}